Repository navigation
Keep a host function in the engine's own realm after a second realm exists - #2893
Merged
Merged
Conversation
…xists An engine keeps the first Intrinsics it built so that constructing a host function does not need a realm passed in — the public ClrFunction(Engine, ...) constructor reads _originalIntrinsics.Function.PrototypeObject for the prototype. That assignment was unconditional, and Intrinsics is constructed once per realm. So constructing a ShadowRealm (or $262.createRealm under test262) replaced it with the new realm's intrinsics, and every host function built afterwards took its prototype from a realm the surrounding script cannot reach. `log instanceof Function` came back false for a global whose delegate happened to materialize after a script had constructed a shadow realm — which a lazily registered global does by design, on first read. Assign only when the slot is still empty. The first Intrinsics an engine builds are the ones InitializeHostDefinedRealm created, which is the engine's principal realm and the one a host means when it builds against the engine; every later set belongs to a second realm and must not displace it. Found while reviewing where per-engine host state belongs (sebastienros#2891): the field is an ad-hoc stand-in for "the principal realm", and it did not hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 14, 2026
legrab
added a commit
to legrab/pocok
that referenced
this pull request
Aug 18, 2026
Updated [Jint](https://github.com/sebastienros/jint) from 4.15.3 to 4.16.0. <details> <summary>Release notes</summary> _Sourced from [Jint's releases](https://github.com/sebastienros/jint/releases)._ ## 4.16.0 Jint 4.16.0 is a **correctness- and reliability-focused release**: alongside asynchronous module loading, proper tail calls and four new iterator built-ins, a pre-tag review swept the whole engine and fixed what it found — including long-standing defects that predate this cycle. **No option defaults changed.** Behaviour changes to note up front: `JSON.stringify` and other machine-readable output now format invariantly under every host culture — under Swedish or Finnish locales on .NET 8+ it used to emit a Unicode minus sign no JSON parser accepts; `JSON.parse` now rejects trailing commas as the grammar requires; bare identifiers at global scope resolve through the global's prototype chain per spec; `IModuleLoader.Resolve` is consulted once per (referrer, specifier) pair, so a loader using it as a per-import access-control checkpoint should move the check to `LoadModule`; and an inconsistent sort comparator now finishes with an implementation-defined order on every target framework instead of hanging (net462/netstandard) or throwing a CLR exception at script (net8+). ### Highlights **Proper tail calls (#2975).** Strict-mode calls in tail position reuse their frame, so `"use strict"` tail recursion runs in constant stack — the first ES2015 PTC implementation among the .NET engines. **Asynchronous module loading (#2872).** `IAsyncModuleLoader` and the `AsyncModuleLoader` template let a host fetch module source over I/O without blocking a thread; `Engine.Modules.StartImport` returns an operation a game loop drives via `ProcessTasks()`, and `ImportAsync` awaits without holding a thread. The spec's load phase now exists as written, a warm-cache async loader keeps the blocking `Import` fully synchronous, and the blocking drain wakes on a work-arrived signal instead of polling. A module served over a transport keeps its whole url as `Module.Location` so its own relative imports resolve, a deferred namespace evaluates its module instead of exposing uninitialized bindings, and an import abandoned by a global snapshot restore reports itself faulted instead of polling forever. **The process no longer dies for recoverable reasons.** `Options.LimitRecursion` used to kill the host process for most useful limits — the constraint fired, and the unwind itself overflowed the stack; exception filters now let it unwind ~7× deeper. The new opt-in `Options.Constraints.StackOverflowGuard` converts unbounded recursion — reachable through eighteen distinct routes, `new`, accessors, coercions and Proxy traps included — from a process kill into a catchable `RangeError`, exempting strict tail calls, which grow no stack. And a family of CLR exceptions that escaped `engine.Evaluate` past every script `catch` are now proper JavaScript errors or correct results: sorting with an inconsistent comparator, destructuring with a function-valued default (`const { onChange = () => {} } = opts`), `toLocaleString` outside `DateTime`'s range, typed-array `defineProperty` without a value, `DataView` reads at 2³¹, `String.replace` `$'` with a lying exec, and the first instant of year 10000. **New built-ins.** `Iterator.prototype.join`, `chunks`, `windows` and `includes`; `take`/`drop` now throw `RangeError` for a finite limit above 2^53−1 per the updated proposals. `Intl.Locale.prototype.getCollations` reports CLDR-cited collation data that `Intl.Collator` accepts in full, a malformed `collation` option is a `RangeError`, and `Intl.supportedValuesOf("collation")` derives from the same lists so the three can never drift. **Conformance, from a review that ran what the suite does not.** Two of the fixed defects had test262 coverage only under the never-generated `staging/` directory, and several had none at all: `parseInt` strips the sign before testing for a hex prefix, so `parseInt("-0x10")` is −16; a suspended `finally` no longer swallows a pending `break`/`continue`; a Proxy (or exotic host object) as the global's prototype answers bare identifiers through its `get` trap; `Date.prototype.toISOString` emits the spec's six-digit expanded year and round-trips through `Date.parse` in every spelling including year 0; iterator helpers close their receiver exactly once and only when the spec says so, and carry their own `@@toStringTag`; `Map`/`Set` `size` is the prototype accessor the spec defines rather than a phantom own property; a Proxy's `defineProperty` trap receives the partial descriptor the caller wrote; a string's `@@iterator` is read once, with the primitive as receiver; `Array.prototype.join` re-asks the array when a side effect fills a hole mid-join; a direct eval reaches the enclosing function's `arguments` in both modes; and `Temporal.Now` drops the methods the proposal removed. **Embedder surface.** `OperationDeadlineConstraint` bounds a whole multi-entry host operation; `ScriptPreparationOptions.StaticAnalysis` trades prepare-time analysis for per-engine materialization on shared graphs; `ModuleFactory.LocationOf` exposes the module-naming rule a host must match; `Engine.Advanced.HostDefined` carries per-request state on a pooled engine; the CLR exception behind an interop error is reachable through `JintException.TryGetClrException` with opt-in `ChainClrExceptions()`, and a host method's own `TargetException` is no longer mistaken for a receiver mismatch; and a recursion-limit failure propagates out of a module load instead of becoming a catchable rejection. **Performance, gated.** Against v4.15.3 on idle hardware, medians of three paired runs: `controlflow-recursive` **−15.6% time and −40.4% allocation** (proper tail calls), `bitops-3bit-bits-in-byte` −8.9%, `math-spectral-norm` −7.3%, `crypto-sha1` −6.9%, `3d-raytrace` −5.9%, `math-cordic` −5.8%, with a broad −1–4% tail across the call- and string-heavy rows; no row moved outside its own measured cross-run envelope in the other direction, and allocation is flat within ±0.2% suite-wide. Warmed `parseInt` call sites take the frameless fast-call lane (−13% on the parse loop), joined by the `Number` predicates, `String.prototype.indexOf`/`startsWith`/`endsWith`/`includes`/`at`/`substr`, global `isNaN`/`isFinite` and `Array.isArray` (−3% to −19%) and the `Map`/`Set` method family (`map.get` hit loop −13%); existence questions on a wrapped dictionary answer from `ContainsKey`, taking `in` −33% with −98% allocation and `Object.keys` −37%; resolving an inherited global no longer allocates per miss (−99.99% on the read loop) and a global created through an inherited write keeps the in-place store; JSON replacer/reviver eligibility is decided once per document, built-in callback dispatch once per loop, a call site's arguments reach an interpreted callee in registers, and function-local `let`/`const` live in fixed slots. **Breaking changes.** `Int32Extensions`/`Int64Extensions`/`DoubleExtensions` — polyfill hosts that leaked into the public API — are now internal; on net462/netstandard2.0, code with `using Jint;` may have bound span `Parse`/`TryParse` members through them. `JsonParser` rejects trailing commas. `Number.parseInt.length`/`Number.parseFloat.length` report their spec values. Post-construction mutation of an `Options` instance no longer reaches an already-built engine, and `Options.Configure` callbacks work again. `UnwrapIfPromise` reports a cancelled engine as `ExecutionCanceledException` instead of a timeout. Time-zone matching is ASCII-case-insensitive per ECMA-402. On the [engine comparison benchmarks](https://github.com/sebastienros/jint/blob/main/Jint.Benchmark/README.md), Jint 4.16.0 is the fastest engine outright on 5 of 12 scripts — leading `dromaeo-object-regexp-modern` over native V8 by 1.25× — in a statistical tie for first on `interop-collection-traversal`, the fastest managed engine on 10 of 12, the fastest interpreter on all 12, and 8.6×–11.2× ahead of ClearScript (native V8) on every interop row while allocating 3.9×–12.4× less than the nearest managed competitor. ## What's Changed * Run the repository's own host tests under Release-mode contract verification by @lahma in sebastienros/jint#2866 * Update test262 suite and implement Iterator.prototype.join by @lahma in sebastienros/jint#2867 * Stop a closing iterator from swallowing the error that closed it by @lahma in sebastienros/jint#2868 * Give every benchmark row its own engine by @lahma in sebastienros/jint#2873 * Hand a call site's arguments to an interpreted callee in registers by @lahma in sebastienros/jint#2874 * Ask once per loop, not once per element, how to call a built-in's callback by @lahma in sebastienros/jint#2876 * Say which spec document to read for a feature by @lahma in sebastienros/jint#2882 * Run PR CI on every pull request, not only those targeting main by @lahma in sebastienros/jint#2883 * Update test262 suite and adopt the new take/drop RangeError by @lahma in sebastienros/jint#2877 * Implement Iterator Chunking by @lahma in sebastienros/jint#2878 * Say to write against the modern BCL and polyfill downwards by @lahma in sebastienros/jint#2884 * Implement Iterator Includes by @lahma in sebastienros/jint#2879 * Ask once per document, not once per key, how to call a JSON replacer or reviver by @lahma in sebastienros/jint#2885 * Use double.IsFinite instead of hand-rolled NaN and infinity pairs by @lahma in sebastienros/jint#2880 * Bump the testing group with 1 update by @dependabot[bot] in sebastienros/jint#2889 * Stop charging closures for per-engine lazy globals by @lahma in sebastienros/jint#2890 * Bump the analyzers group with 1 update by @dependabot[bot] in sebastienros/jint#2888 * Make the lazy-global re-arm on restore a contract instead of an accident by @lahma in sebastienros/jint#2892 * Let the amortized constraint cadence span top-level entries into the engine by @lahma in sebastienros/jint#2886 * Let a function's own let/const live in its fixed slots by @lahma in sebastienros/jint#2887 * Keep a host function in the engine's own realm after a second realm exists by @lahma in sebastienros/jint#2893 * Make an engine's principal realm [[HostDefined]] reachable by @lahma in sebastienros/jint#2891 * Carry a labelled break/continue target on the completion record by @lahma in sebastienros/jint#2894 * Make Array.prototype.sort stable on every target framework by @lahma in sebastienros/jint#2898 * Stop toSorted and %TypedArray%.sort hanging on an inconsistent comparator by @lahma in sebastienros/jint#2899 * Delete the evaluation context's dead completion channel by @lahma in sebastienros/jint#2895 * Stop shipping the numeric polyfill hosts as public API by @lahma in sebastienros/jint#2901 ... (truncated) Commits viewable in [compare view](sebastienros/jint@v4.15.3...v4.16.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details>
This was referenced Aug 19, 2026
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 25, 2026
…portValue claims the engine Two things a host reaching into a ShadowRealm had every reason to expect and did not get. Both were found while making ShadowRealm.SetValue mirror Engine.SetValue in sebastienros#3321 and deliberately left out of it; both are in the same twelve lines of the same class. SetValue converted its argument against whichever realm the host called from - the principal one - and installed the result on the shadow realm's global object, so the wrapper carried the principal realm's Object.prototype and `company instanceof Object` inside the realm answered false. That is the opposite of what a realm is for: CrossRealmAttributionTests already pins the rule for the other direction, that a built-in attributes what it produces to its own realm rather than to whichever realm happens to be running. Realm-scoped construction is entering that realm's execution context, and nothing else. Engine.Realm is ExecutionContext.Realm, and every interop construction reads it to pick a prototype - JsValue.FromObject through ObjectInstance's base constructor, TypeReference.CreateTypeReference through TypeReferencePrototype, DelegateWrapper outright. ShadowRealmImportValue in this same class already does exactly that, so SetValue now brackets its registration the same way, through a RealmScope that names it. Engine._realmInConstruction is not an alternative: neither nestable nor exception-safe, and it means something else. All six overloads take the scope, the JsValue one included, so the rule has no exceptions to remember. ClrFunction(Engine, ...), HostFunction and Constructor(Engine, string) deliberately keep pinning engine._originalIntrinsics (sebastienros#2893) and are untouched. The residual that leaves - the three members ObjectWrapper's constructor builds eagerly through the first of those are principal-realm functions inside a shadow realm, while every lazily resolved member is correct - is filed as sebastienros#3365. ImportValue took no host-call reservation at all, while driving module loading, linking and evaluation; only the tail drain claimed anything, and only for its own length. It needed nothing but the using: the reservation is same-thread re-entrant, so every EnterHostCall further down nests inside it, and it is not _hostEntryDepth, so nothing about constraint re-arming changes. It cannot deadlock either - ShadowRealmImportValue returns a pending promise and RunAvailableContinuations runs only what is queued. The script-facing ShadowRealm.prototype.importValue goes through ShadowRealmPrototype and never through this method. Tests are in Jint.Tests.PublicInterface: ShadowRealmValueRealmTests, one fact per overload family plus the two guards, five of which fail on main; and HostEngineConcurrencyTests.ConcurrentShadowRealmImportValueIsRejected, which parks a dedicated thread inside IModuleLoader.LoadModule on a signal rather than a clock and fails on main with "no exception was thrown". Docs: v5-migration.md sections 4.22 and 4.23; Jint/Runtime/Interop/AGENTS.md gains the gotcha - a wrapper's prototype comes from the running realm, there is one realm-scoped construction path, and these three constructors deliberately do not follow it. UndocumentedPublicApi.txt 658 to 657, since ImportValue is being changed and ships documented. No API baseline moves. Fixes sebastienros#3324 Fixes sebastienros#3325 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
to lahma/jint
that referenced
this pull request
Aug 25, 2026
…portValue claims the engine Two things a host reaching into a ShadowRealm had every reason to expect and did not get. Both were found while making ShadowRealm.SetValue mirror Engine.SetValue in sebastienros#3321 and deliberately left out of it; both are in the same twelve lines of the same class. SetValue converted its argument against whichever realm the host called from - the principal one - and installed the result on the shadow realm's global object, so the wrapper carried the principal realm's Object.prototype and `company instanceof Object` inside the realm answered false. That is the opposite of what a realm is for: CrossRealmAttributionTests already pins the rule for the other direction, that a built-in attributes what it produces to its own realm rather than to whichever realm happens to be running. Realm-scoped construction is entering that realm's execution context, and nothing else. Engine.Realm is ExecutionContext.Realm, and every interop construction reads it to pick a prototype - JsValue.FromObject through ObjectInstance's base constructor, TypeReference.CreateTypeReference through TypeReferencePrototype, DelegateWrapper outright. ShadowRealmImportValue in this same class already does exactly that, so SetValue now brackets its registration the same way, through a RealmScope that names it. Engine._realmInConstruction is not an alternative: neither nestable nor exception-safe, and it means something else. All six overloads take the scope, the JsValue one included, so the rule has no exceptions to remember. ClrFunction(Engine, ...), HostFunction and Constructor(Engine, string) deliberately keep pinning engine._originalIntrinsics (sebastienros#2893) and are untouched. The residual that leaves - the three members ObjectWrapper's constructor builds eagerly through the first of those are principal-realm functions inside a shadow realm, while every lazily resolved member is correct - is filed as sebastienros#3365. ImportValue took no host-call reservation at all, while driving module loading, linking and evaluation; only the tail drain claimed anything, and only for its own length. It needed nothing but the using: the reservation is same-thread re-entrant, so every EnterHostCall further down nests inside it, and it is not _hostEntryDepth, so nothing about constraint re-arming changes. It cannot deadlock either - ShadowRealmImportValue returns a pending promise and RunAvailableContinuations runs only what is queued. The script-facing ShadowRealm.prototype.importValue goes through ShadowRealmPrototype and never through this method. Tests are in Jint.Tests.PublicInterface: ShadowRealmValueRealmTests, one fact per overload family plus the two guards, five of which fail on main; and HostEngineConcurrencyTests.ConcurrentShadowRealmImportValueIsRejected, which parks a dedicated thread inside IModuleLoader.LoadModule on a signal rather than a clock and fails on main with "no exception was thrown". Docs: v5-migration.md sections 4.22 and 4.23; Jint/Runtime/Interop/AGENTS.md gains the gotcha - a wrapper's prototype comes from the running realm, there is one realm-scoped construction path, and these three constructors deliberately do not follow it. UndocumentedPublicApi.txt 658 to 657, since ImportValue is being changed and ships documented. No API baseline moves. Fixes sebastienros#3324 Fixes sebastienros#3325 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
added a commit
that referenced
this pull request
Aug 25, 2026
…portValue claims the engine (#3367) Two things a host reaching into a ShadowRealm had every reason to expect and did not get. Both were found while making ShadowRealm.SetValue mirror Engine.SetValue in #3321 and deliberately left out of it; both are in the same twelve lines of the same class. SetValue converted its argument against whichever realm the host called from - the principal one - and installed the result on the shadow realm's global object, so the wrapper carried the principal realm's Object.prototype and `company instanceof Object` inside the realm answered false. That is the opposite of what a realm is for: CrossRealmAttributionTests already pins the rule for the other direction, that a built-in attributes what it produces to its own realm rather than to whichever realm happens to be running. Realm-scoped construction is entering that realm's execution context, and nothing else. Engine.Realm is ExecutionContext.Realm, and every interop construction reads it to pick a prototype - JsValue.FromObject through ObjectInstance's base constructor, TypeReference.CreateTypeReference through TypeReferencePrototype, DelegateWrapper outright. ShadowRealmImportValue in this same class already does exactly that, so SetValue now brackets its registration the same way, through a RealmScope that names it. Engine._realmInConstruction is not an alternative: neither nestable nor exception-safe, and it means something else. All six overloads take the scope, the JsValue one included, so the rule has no exceptions to remember. ClrFunction(Engine, ...), HostFunction and Constructor(Engine, string) deliberately keep pinning engine._originalIntrinsics (#2893) and are untouched. The residual that leaves - the three members ObjectWrapper's constructor builds eagerly through the first of those are principal-realm functions inside a shadow realm, while every lazily resolved member is correct - is filed as #3365. ImportValue took no host-call reservation at all, while driving module loading, linking and evaluation; only the tail drain claimed anything, and only for its own length. It needed nothing but the using: the reservation is same-thread re-entrant, so every EnterHostCall further down nests inside it, and it is not _hostEntryDepth, so nothing about constraint re-arming changes. It cannot deadlock either - ShadowRealmImportValue returns a pending promise and RunAvailableContinuations runs only what is queued. The script-facing ShadowRealm.prototype.importValue goes through ShadowRealmPrototype and never through this method. Tests are in Jint.Tests.PublicInterface: ShadowRealmValueRealmTests, one fact per overload family plus the two guards, five of which fail on main; and HostEngineConcurrencyTests.ConcurrentShadowRealmImportValueIsRejected, which parks a dedicated thread inside IModuleLoader.LoadModule on a signal rather than a clock and fails on main with "no exception was thrown". Docs: v5-migration.md sections 4.22 and 4.23; Jint/Runtime/Interop/AGENTS.md gains the gotcha - a wrapper's prototype comes from the running realm, there is one realm-scoped construction path, and these three constructors deliberately do not follow it. UndocumentedPublicApi.txt 658 to 657, since ImportValue is being changed and ships documented. No API baseline moves. Fixes #3324 Fixes #3325 Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Sep 1, 2026
…builds eagerly, belong to that realm (backport of #3367 and #3325/#3365) (#3557) Backport of the two ShadowRealm realm-affinity fixes, adapted to 4.x. They land together because the second only bites once the first exists: an ObjectWrapper takes its realm from whichever one is running when it is built, so until SetValue enters the shadow realm there is no shadow realm for the eagerly built members to belong to. SetValue converted its argument against whichever realm the host called from - the principal one - and then installed the result on the shadow realm's global object, so the wrapper carried the principal realm's Object.prototype and `company instanceof Object` inside the realm answered false. That is the opposite of what a realm is for. Realm-scoped construction is entering that realm's execution context and nothing else: Engine.Realm is ExecutionContext.Realm, and every interop construction reads it to pick a prototype - JsValue.FromObject through ObjectInstance's base constructor, TypeReference.CreateTypeReference through TypeReferencePrototype, DelegateWrapper outright. ShadowRealmImportValue in this same class already does exactly that, so SetValue now brackets its registration the same way through a RealmScope that names it. Three overloads take the scope on 4.x rather than main's six, and that is the same rule rather than a narrower one: 4.x has no SetValue(string, Type), no generic and no array overload, so a Type and an array both arrive through SetValue(string, object), and the four primitive overloads reach the JsValue one, which takes the scope as well. Installation happening in the realm being written is one rule rather than two. ObjectWrapper's constructor then builds three members eagerly - Symbol.dispose, Symbol.asyncDispose and toJSON - through the public ClrFunction(Engine, string, ...) constructor, which pins engine._originalIntrinsics. That pin is deliberate and is what #2893 fixed: a function a *host* wires up against an engine must belong to the realm the surrounding script can reach whatever realm happened to be current when it was built. It is the wrong constructor for a function the engine builds for an object it is creating, and the consequence was two answers on one object inside a shadow realm - `handle.Dispose instanceof Function` true while `handle[Symbol.dispose] instanceof Function` was false. Each now uses the internal ClrFunction(Engine, Realm, ...) constructor whose own doc comment names this case, with engine.Realm - the same realm ObjectInstance's constructor just took the object's own prototype from. ClrFunction(Engine, ...), HostFunction and Constructor(Engine, string) are untouched, and HostClrFunctionRealmTests still passes. The other half of #3367 - ImportValue taking a host-call reservation - is deliberately not here: 4.x has no EnterHostCall and no engine-ownership reservation at all, so there is nothing to claim and no HostEngineConcurrencyTests to join. Tests are Jint.Tests.PublicInterface/ShadowRealmValueRealmTests, translated to xUnit: one fact per converting overload, one per eagerly built member, both negative halves stated against the principal intrinsic handed into the realm through the JsValue overload, the two guards that an engine registration keeps the principal realm, and the control that disposal itself still works. Failing first on unfixed 4.x: 10 of 13 on net10.0, 9 of 12 on net472. Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Sep 1, 2026
This was referenced Sep 9, 2026
This was referenced Sep 17, 2026
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
An engine keeps the first
Intrinsicsit builds inEngine._originalIntrinsicsso that constructing a host function does not need a realm passed in — the publicClrFunction(Engine, ...)constructor reads_originalIntrinsics.Function.PrototypeObject.Intrinsics's constructor assigned that field unconditionally, andIntrinsicsis constructed once per realm. Constructing aShadowRealm— or$262.createRealm()under test262 — therefore replaced it, and every host function built afterwards took its prototype from a realm the surrounding script cannot reach.Why it is reachable rather than theoretical
A lazily registered global builds its value on first read, which can easily be after a script has constructed a shadow realm. The internal
ClrFunction(Engine, Realm, ...)overload exists precisely to dodge this for in-box shape materialization; the public constructor had no such protection, and it is the one embedders use.The fix
The first intrinsics an engine builds are the ones
InitializeHostDefinedRealmcreated — the engine's principal realm, and the realm a host means when it builds a function against the engine. Every later set belongs to a second realm and must not displace it.Tests
Jint.Tests.PublicInterface/HostClrFunctionRealmTests.cs, asserting what a script can observe (PrototypeObjectis internal, which is the right constraint for a test about host-visible behaviour):ShadowRealmstill does — fails without this change;ShadowRealmis stillinstanceof Function— the reachable shape, and also fails without this change.I confirmed all three fail on
mainand pass with the fix.Jint.Tests4693 andJint.Tests.PublicInterface1266 pass on net10.0 and net472.Where it came from
Reviewing where per-engine host state belongs (#2891).
_originalIntrinsicsis an ad-hoc stand-in for "this engine's principal realm", and it did not hold — which is part of the argument for naming that concept properly.🤖 Generated with Claude Code